import json
import os
import re
from typing import Any, Dict, List, Mapping, Sequence

import markdown
from django.utils.html import escape as escape_html
from markdown.extensions import Extension
from markdown.preprocessors import Preprocessor

from zerver.lib.markdown.preprocessor_priorities import PREPROCESSOR_PRIORITES
from zerver.openapi.openapi import (
    check_deprecated_consistency,
    get_openapi_parameters,
    get_parameters_description,
)

REGEXP = re.compile(r"\{generate_api_arguments_table\|\s*(.+?)\s*\|\s*(.+)\s*\}")


class MarkdownArgumentsTableGenerator(Extension):
    def __init__(self, configs: Mapping[str, Any] = {}) -> None:
        self.config = {
            "base_path": [
                ".",
                "Default location from which to evaluate relative paths for the JSON files.",
            ],
        }
        for key, value in configs.items():
            self.setConfig(key, value)

    def extendMarkdown(self, md: markdown.Markdown) -> None:
        md.preprocessors.register(
            APIArgumentsTablePreprocessor(md, self.getConfigs()),
            "generate_api_arguments",
            PREPROCESSOR_PRIORITES["generate_api_arguments"],
        )


class APIArgumentsTablePreprocessor(Preprocessor):
    def __init__(self, md: markdown.Markdown, config: Mapping[str, Any]) -> None:
        super().__init__(md)
        self.base_path = config["base_path"]

    def run(self, lines: List[str]) -> List[str]:
        done = False
        while not done:
            for line in lines:
                loc = lines.index(line)
                match = REGEXP.search(line)

                if not match:
                    continue

                filename = match.group(1)
                doc_name = match.group(2)
                filename = os.path.expanduser(filename)

                is_openapi_format = filename.endswith(".yaml")

                if not os.path.isabs(filename):
                    parent_dir = self.base_path
                    filename = os.path.normpath(os.path.join(parent_dir, filename))

                if is_openapi_format:
                    endpoint, method = doc_name.rsplit(":", 1)
                    arguments: List[Dict[str, Any]] = []

                    try:
                        arguments = get_openapi_parameters(endpoint, method)
                    except KeyError as e:
                        # Don't raise an exception if the "parameters"
                        # field is missing; we assume that's because the
                        # endpoint doesn't accept any parameters
                        if e.args != ("parameters",):
                            raise e
                else:
                    with open(filename) as fp:
                        json_obj = json.load(fp)
                        arguments = json_obj[doc_name]

                if arguments:
                    text = self.render_table(arguments)
                # We want to show this message only if the parameters
                # description doesn't say anything else.
                elif is_openapi_format and get_parameters_description(endpoint, method) == "":
                    text = ["This endpoint does not accept any parameters."]
                else:
                    text = []
                # The line that contains the directive to include the macro
                # may be preceded or followed by text or tags, in that case
                # we need to make sure that any preceding or following text
                # stays the same.
                line_split = REGEXP.split(line, maxsplit=0)
                preceding = line_split[0]
                following = line_split[-1]
                text = [preceding, *text, following]
                lines = lines[:loc] + text + lines[loc + 1 :]
                break
            else:
                done = True
        return lines

    def render_table(self, arguments: Sequence[Mapping[str, Any]]) -> List[str]:
        # TODO: Fix naming now that this no longer renders a table.
        table = []
        argument_template = """
<div class="api-argument" id="parameter-{argument}">
    <p class="api-argument-name"><strong>{argument}</strong> <span class="api-field-type">{type}</span> {required} {deprecated} <a href="#parameter-{argument}" class="api-argument-hover-link"><i class="fa fa-chain"></i></a></p>
    <div class="api-example">
        <span class="api-argument-example-label">Example</span>: <code>{example}</code>
    </div>
    <div class="api-description">{description}</div>
    <hr>
</div>"""

        md_engine = markdown.Markdown(extensions=[])
        arguments = sorted(arguments, key=lambda argument: "deprecated" in argument)
        for argument in arguments:
            description = argument["description"]
            oneof = ["`" + str(item) + "`" for item in argument.get("schema", {}).get("enum", [])]
            if oneof:
                description += "\nMust be one of: {}.".format(", ".join(oneof))

            default = argument.get("schema", {}).get("default")
            if default is not None:
                description += f"\nDefaults to `{json.dumps(default)}`."
            data_type = ""
            if "schema" in argument:
                data_type = generate_data_type(argument["schema"])
            else:
                data_type = generate_data_type(argument["content"]["application/json"]["schema"])

            # TODO: OpenAPI allows indicating where the argument goes
            # (path, querystring, form data...).  We should document this detail.
            example = ""
            if "example" in argument:
                # We use this style without explicit JSON encoding for
                # integers, strings, and booleans.
                # * For booleans, JSON encoding correctly corrects for Python's
                #   str(True)="True" not matching the encoding of "true".
                # * For strings, doing so nicely results in strings being quoted
                #   in the documentation, improving readability.
                # * For integers, it is a noop, since json.dumps(3) == str(3) == "3".
                example = json.dumps(argument["example"])
            else:
                example = json.dumps(argument["content"]["application/json"]["example"])

            required_string: str = "required"
            if argument.get("in", "") == "path":
                # Any path variable is required
                assert argument["required"]
                required_string = "required in path"

            if argument.get("required", False):
                required_block = f'<span class="api-argument-required">{required_string}</span>'
            else:
                required_block = '<span class="api-argument-optional">optional</span>'

            check_deprecated_consistency(argument, description)
            if argument.get("deprecated", False):
                deprecated_block = '<span class="api-argument-deprecated">Deprecated</span>'
            else:
                deprecated_block = ""

            table.append(
                argument_template.format(
                    argument=argument.get("argument") or argument.get("name"),
                    example=escape_html(example),
                    required=required_block,
                    deprecated=deprecated_block,
                    description=md_engine.convert(description),
                    type=data_type,
                )
            )

        return table


def makeExtension(*args: Any, **kwargs: str) -> MarkdownArgumentsTableGenerator:
    return MarkdownArgumentsTableGenerator(kwargs)


def generate_data_type(schema: Mapping[str, Any]) -> str:
    data_type = ""
    if "oneOf" in schema:
        for item in schema["oneOf"]:
            data_type = data_type + generate_data_type(item) + " | "
        data_type = data_type[:-3]
    elif "items" in schema:
        data_type = "(" + generate_data_type(schema["items"]) + ")[]"
    else:
        data_type = schema["type"]
    return data_type
